﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Security;
using System.Threading;
using System.Windows.Forms;

namespace ClownStopper
{

    #region Interfaces


    public interface IMessage
    {
        /*MessageType*/
        int Type { get; }

        Byte[] GetMessagePacket();

        bool Parse(Byte[] data, out string error);
        IMessage Clone();
    }

    public interface IMessageClient
    {
        int ListenPort { get; set; }

        int ServerPort { get; set; }

        String Server { get; set; }

        void Initialize();

        void SendMessage(IMessage message);
        void SendMessageAsync(IMessage message);
    }

    #endregion

    public class MessageClient : IMessageClient
    {
        #region private members
        private Socket _mClientSocket;
        private IPAddress localAddress;
        private Socket _mListenerSocket;
        private event OnReceiveMessageDelegate ClientReceiveMessage;
        private event OnErrorDelegate ClientReceiveError;
        private readonly Dictionary<int, IMessage> _possibleMessages;
        #endregion

        public MessageClient(int listenPort, string server, int serverPort, string group)
        {
            _possibleMessages = new Dictionary<int, IMessage>();
            _possibleMessages[((int)MessageType.TextDataMessage)] = new TextMessage();
            _possibleMessages[((int)MessageType.BingoMessage)] = new BingoMessage();

            ListenPort = listenPort;
            ServerPort = serverPort;
            Server = server;
            Group = group;
        }

        public event OnReceiveMessageDelegate OnClientReceiveMessage
        {
            add { ClientReceiveMessage += value; }
            remove { ClientReceiveMessage -= value; }
        }

        public event OnErrorDelegate OnClientReceiveError
        {
            add { ClientReceiveError += value; }
            remove { ClientReceiveError -= value; }
        }

        #region Properties
        public string Group { get; set; }

        public int ListenPort { get; set; }

        public IPAddress LocalIp {
            get {
                if (localAddress == null)
                {
                    localAddress = FindLocalAddress();
                }
                return localAddress;
            }
        }

        public int ServerPort { get; set; }

        public string Server { get; set; } 
        #endregion

        public void Initialize()
        {
            if (ListenPort <= 0 || ListenPort >= 65536)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("InvalidListenPort"));
                return;
            }

            if (ServerPort <= 0 || ServerPort >= 65536)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("InvalidServerPort"));
                return;
            }

            if (string.IsNullOrEmpty(Server))
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("InvalidServer"));
                return;
            }

            localAddress = FindLocalAddress();

            if (localAddress == null)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("ErrorNoAvailableIPAddress"));
                return;
            }

            try
            {
                //register on the server
                var regMessage = new RegisteredClient();
                regMessage.Group = Group;
                regMessage.Client = new MessageClientDetails();
                regMessage.Client.ClientIPAddress = LocalIp.ToString();
                regMessage.Client.ClientListenPort = ListenPort;

                SendMessage(regMessage);
            }
            catch (ArgumentNullException ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("Invalid Endpoint supplied : " + ex.Message));
               
            }
            catch (SocketException ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("Error attempting to access the socket : " + ex.Message));
                
            }
            catch (ObjectDisposedException ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("The socket was closed : " + ex.Message));
                
            }
            catch (SecurityException ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("No permission for the requested operation : " + ex.Message));
                
            }
            catch (InvalidOperationException ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("Invalid operation, the Socket is listening: " + ex.Message));
                
            }
            catch (Exception ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("There was an unknown error : " + ex.Message));
                
            }

            
        }

        private IPAddress FindLocalAddress()
        {
            IPHostEntry hostEntry = Dns.GetHostEntry( /*"localhost"*/Dns.GetHostName()); //Dns.Resolve("localhost").AddressList[0];

            if (hostEntry.AddressList.Length <= 0)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("ErrorNoAvailableIPAddress"));
                return null;
            }

            for (int i = 0; i < hostEntry.AddressList.Length; ++i)
            {
                if (hostEntry.AddressList[i].AddressFamily == AddressFamily.InterNetwork)
                    return hostEntry.AddressList[i];
            }

            return  null;
        }

        #region Send Messages to Server
        public void SendMessage(IMessage message)
        {
            _mClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress remoteMachine = IPAddress.Parse(Server);
            var remoteEndpoint = new IPEndPoint(remoteMachine, ServerPort);
            try
            {
                _mClientSocket.Connect(remoteEndpoint);
            }
            catch (Exception ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("An error occurred while attempting to connnect : " + ex.Message));
                return;
            }
            if (_mClientSocket.Connected)
                _mClientSocket.Send(message.GetMessagePacket());
            _mClientSocket.Close();
            _mClientSocket = null;
        }

        public void SendMessageAsync(IMessage message)
        {
            _mClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress remoteMachine = IPAddress.Parse(Server);
            var remoteEndpoint = new IPEndPoint(remoteMachine, ServerPort);
            try
            {
                _mClientSocket.BeginConnect(remoteEndpoint, OnHandleConnection, message);
            }
            catch (Exception ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("An error occurred while attempting to connnect : " + ex.Message));
                return;
            }
        }

        protected void OnHandleConnection(IAsyncResult asyncResult)
        {
            try
            {
                //mClientSocket.BeginConnect(remoteEndpoint, new AsyncCallback(OnHandleConnection), mClientSocket);
                var message = (IMessage)asyncResult.AsyncState;
                _mClientSocket.EndConnect(asyncResult);
                Byte[] dataBuffer = message.GetMessagePacket();
                var msgPacket = new TxRxPacket(_mClientSocket, dataBuffer.Length);
                msgPacket.mDataBuffer = dataBuffer;
                if (_mClientSocket.Connected)
                    _mClientSocket.BeginSend(msgPacket.mDataBuffer, 0, msgPacket.mDataBuffer.Length, SocketFlags.None, new AsyncCallback(OnHandleSend), msgPacket);
            }
            catch (Exception ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("An error occurred while attempting to connnect : " + ex.Message));
                return;
            }
        }

        protected void OnHandleSend(IAsyncResult asyncResult)
        {
            try
            {
                _mClientSocket.EndSend(asyncResult);
                //TxRxPacket msgPacket = (TxRxPacket)asyncResult.AsyncState;

                _mClientSocket.Close();
                _mClientSocket = null;
            }
            catch (Exception ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("An error occurred while attempting to connnect : " + ex.Message));
                return;
            }
        }

        #endregion

        #region Receive Messages From Server
        public void SetupResponseListener()
        {
            //set up for the receive
            try
            {
                //create a listening socket
                if (_mListenerSocket == null)
                {
                    _mListenerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                }
                var localIP = new IPEndPoint(LocalIp /*IPAddress.Any*/, ListenPort);
                _mListenerSocket.Bind(localIP);
                _mListenerSocket.Listen(50); //TODO -> ponder which is the best value to use here
                _mListenerSocket.BeginAccept(new AsyncCallback(OnHandleClientConnection), null);
            }
            catch (SocketException ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("Unable to create the socket : " + ex.Message));
            }
            catch (ObjectDisposedException ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("The socket was forcefully closed : " + ex.Message));
            }
            catch (Exception ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs(ex.Message));
            }
        }

        protected void OnHandleClientConnection(IAsyncResult asyncResult)
        {
            try
            {
                Socket workerSocket = _mListenerSocket.EndAccept(asyncResult);

                try
                {
                    var dataStatus = new TxRxPacket(workerSocket);
                    workerSocket.BeginReceive(dataStatus.mDataBuffer, 0, dataStatus.mDataBuffer.Length, SocketFlags.None, new AsyncCallback(OnHandleClientData), dataStatus);
                }
                catch (Exception ex)
                {
                    if (ClientReceiveError != null)
                        ClientReceiveError.Invoke(this, new ErrorEventArgs(ex.Message));
                }

                _mListenerSocket.BeginAccept(new AsyncCallback(OnHandleClientConnection), null);
            }
            catch (ObjectDisposedException ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("Socket has been closed : "+ ex.Message));
            }
            catch (Exception ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs(ex.Message));
            }
        }

        private IMessage ParseMessage(Byte[] data)
        {
            string error;
            foreach (var messageByType in _possibleMessages)
            {
                if (messageByType.Value.Parse(data, out error))
                {
                    return messageByType.Value.Clone();
                }
                if (!string.IsNullOrEmpty(error))
                {
                    if (ClientReceiveError != null)
                        ClientReceiveError.Invoke(this, new ErrorEventArgs("Parse Error: " + error));
                }
            }
            return null; //TODO -> use this instead? : new EmptyMessage();
        }

        protected void OnHandleClientData(IAsyncResult asyncResult)
        {
            try
            {
                var dataStatus = (TxRxPacket)asyncResult.AsyncState;

                dataStatus.mCurrentSocket.EndReceive(asyncResult);
                dataStatus.StoreCurrentData();
                var ms = ParseMessage(dataStatus.mStoredBuffer);
                IMessage rxMessage = null;
                string error;
                if (ms.Parse(dataStatus.mStoredBuffer, out error))
                {
                    rxMessage = ms.Clone();
                }
                if (!string.IsNullOrEmpty(error))
                {
                    if (ClientReceiveError != null)
                        ClientReceiveError.Invoke(this, new ErrorEventArgs("Parse Error: " + error));
                }

                if (rxMessage == null)
                {
                    //receive the rest of the message
                    dataStatus.mCurrentSocket.BeginReceive(dataStatus.mDataBuffer, 0, dataStatus.mDataBuffer.Length, SocketFlags.None, new AsyncCallback(OnHandleClientData), dataStatus);
                    return;
                }

                if (ClientReceiveMessage != null)
                    ClientReceiveMessage.Invoke(this, new ReceiveMessageEventArgs(rxMessage));
            }
            catch (ObjectDisposedException ex)
            {
                if (ClientReceiveError != null)
                    ClientReceiveError.Invoke(this, new ErrorEventArgs("Socket has been closed : " + ex.Message));
            }
            catch (SocketException ex)
            {
                if (ex.ErrorCode == 10054) // Error code for Connection reset by peer
                {
                    if (ClientReceiveError != null)
                        ClientReceiveError.Invoke(this, new ErrorEventArgs("Connection reset by peer : " + ex.Message));
                }
                else
                {
                    if (ClientReceiveError != null)
                        ClientReceiveError.Invoke(this, new ErrorEventArgs(ex.Message));
                }
            }
        }
        #endregion

        public void Close()
        {
            var msg = new UnregisteredClient { Group = Group, Client = new MessageClientDetails { ClientIPAddress = localAddress.ToString(), ClientListenPort = ListenPort } };
            SendMessageAsync(msg);

            Thread.Sleep(100); //Sleep required due to the use of asynchronous methods
        }

        ~MessageClient()
        {
            try
            {
                Close();
            }
            catch
            {
                
            }
            if (_mClientSocket != null)
                _mClientSocket.Close();
        }

    }



 
}